﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using Microsoft.DirectX;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.DirectX.Direct3D;



namespace robotGUI
{
    public partial class DxControl : UserControl
    {
        //DirectX device
        Device m_device = null;
        bool dxBroken = false;
        
        //Objects
        Mesh oldCarMesh = null; //Old car with fixed wheels
        private Material[] oldCarMaterials;
        private Texture[] oldCarTextures;
        Mesh carMesh = null;    //Car body
        private Material[] carMaterials;
        private Texture[] carTextures;
        Mesh wheelMesh = null;  //Car wheel
        private Material[] wheelMaterials;
        private Texture[] wheelTextures;
        Mesh arrowMesh = null;  //Vector arrow
        private Material[] arrowMaterials;
        private Texture[] arrowTextures;

        //Movement trackers
        float lastWheelDistance = 0;
        float xOffs = 0;
        float yOffs = 0;

        //Misc
        private Assembly assembly;  //My executing assembly
        public Form Owner;          //Container form that wants a function "resizeViewport(width)" to handle ratio lock

        //Design mode hack
        void dmRefresh() //Design mode only screen refresh
        {
            if (this.DesignMode) this.Invalidate();
        }

        //Car Location can be used to raise car above floor or move car from center of shot
        Matrix _carLocation = Matrix.Identity;
        [DescriptionAttribute("Car Location Matrix. Sets the position, scale and rotation of the car in the scene.")]
        public Matrix CarLocation { get { return _carLocation; } set { _carLocation = value; } }
        //World Location can be used to place world (viewer) anywhere in scene
        Matrix _worldLocation = Matrix.Identity;
        [DescriptionAttribute("World Location Matrix. Sets the position, scale and rotation of the world in the scene.")]
        public Matrix WorldLocation { get { return _worldLocation; } set { _worldLocation = value; } }
        //Left and right wheel distance provide wheel rolling and floor movement.
        float _leftWheelDistance = 0;
        [DescriptionAttribute("Left wheel travel distance. Rotates left wheels to the given angle and shifts the floor to suit.")]
        public float LeftWheelDistance { get { return rToD(_leftWheelDistance); } set { _leftWheelDistance = dToR(value); dmRefresh(); } }
        float _rightWheelDistance = 0;
        [DescriptionAttribute("Right wheel travel distance. Rotates right wheels to the given angle and shifts the floor to suit.")]
        public float RightWheelDistance { get { return rToD(_rightWheelDistance); } set { _rightWheelDistance = dToR(value); dmRefresh(); } }
        //Car pitch and roll provide vehicle tilt and height compensation
        float _carPitch = 0;
        [DescriptionAttribute("Car Pitch Angle. Sets the pitch of the car forwards and backwards.")]
        public float CarPitch { get { return rToD(_carPitch); } set { _carPitch = dToR(value); dmRefresh(); } }
        float _carRoll = 0;
        [DescriptionAttribute("Car Roll Angle. Sets the roll of the car side to side.")]
        public float CarRoll { get { return rToD(_carRoll); } set { _carRoll = dToR(value); dmRefresh(); } }
        //Arrow controls for indication direction of vector arrow
        bool _enableArrow = false;
        [DescriptionAttribute("Enable Arrow. Whether to display the arrow or not.")]
        public bool EnableArrow { get { return _enableArrow; } set { _enableArrow = value; dmRefresh(); } }
        float _arrowAngle = 0;
        [DescriptionAttribute("Arrow Angle. Sets the angle of the arrow forwards and backwards.")]
        public float ArrowAngle { get { return rToD(_arrowAngle); } set { _arrowAngle = dToR(value); dmRefresh(); } }
        //Auto screen refresh at a fixed frame rate. Good for asynchronos (real time) data updates.  
        bool _autoRefresh;
        [DescriptionAttribute("Auto Refresh. Sets the frame rate to 50Hz. When disaled, it is the programmer's responsibility to periodically call dxControl.Invalidate() to redraw the scene.")]
        public bool AutoRefresh
        {
            get { return _autoRefresh; }
            set
            {
                _autoRefresh = value;
                if (!this.DesignMode) timer1.Enabled = value;
            }
        }
        //Basic camera control
        float _viewHeight = 384;
        [DescriptionAttribute("Viewer's height above the floor.")]
        public float ViewHeight { get { return _viewHeight; } set { _viewHeight = value; dmRefresh(); } }
        float _viewAngle = 0;
        [DescriptionAttribute("Angle of the viewer around the vehicle.")]
        public float ViewAngle
        {
            get { return rToD(_viewAngle); }
            set
            {
                _viewAngle = dToR(value);
                _worldLocation = Matrix.RotationY(_viewAngle);
                dmRefresh();
            }
        }
        float _viewDistance = 1500;
        [DescriptionAttribute("Viewer's distance from objective.")]
        public float ViewDistance { get { return _viewDistance; } set { _viewDistance = value; dmRefresh(); } }
        Matrix _cameraView;
        bool _lockAspect = false;
        [DescriptionAttribute("Lock Aspect Ratio. Hold the aspect ratio to 4:3.")]
        public bool LockAspect { get { return _lockAspect; } set { _lockAspect = value; dmRefresh(); } }
        bool _mouseControl = true;
        [DescriptionAttribute("Mouse Control. Whether to allow the user to alter camera angles using the mouse.")]
        public bool MouseControl { get { return _mouseControl; } set { _mouseControl = value; } }

        //Radians to degrees
        float rToD(float r) { return r * (180F / (float)(Math.PI)); }
        //Degrees to radians
        float dToR(float d) { return d * ((float)(Math.PI) / 180F); }
        private Matrix CameraView
        {
            get { return _cameraView; }
            set
            {
                if (m_device != null) m_device.Transform.View = value;
                _cameraView = value;
            }
        }

        //----------------------------------------------------------
        //Constructors
        public DxControl()
        {
            InitializeComponent();
            string errorMessage;
            if (dxBroken = ((errorMessage = InitializeGraphics()) != null))
            {   //DirectX will not initialise..
                splitContainer1.Visible = true;
                lblDxStatus.Visible = true;
                lblDxStatus.Text = errorMessage;
                return;
            }
            this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true);
            assembly = Assembly.GetExecutingAssembly();
            //Bring in objects
            LoadMesh(Properties.Resources.carOld, ref oldCarMesh, ref oldCarMaterials, ref oldCarTextures);
            LoadMesh(Properties.Resources.Wheel1, ref wheelMesh, ref wheelMaterials, ref wheelTextures);
            LoadMesh(Properties.Resources.car1, ref carMesh, ref carMaterials, ref carTextures);
            LoadMesh(Properties.Resources.Arrow, ref arrowMesh, ref arrowMaterials, ref arrowTextures);
            ConstructFloor();
            MouseWheel += new MouseEventHandler(DxControl_MouseWheel);
        }

        public DxControl(Form Owner) : this() 
        {
            this.Owner = Owner;
        }

        //DirectX initialization and maintenance
        bool antiAlias = false;
        MultiSampleType multiSampleType = MultiSampleType.FourSamples;
        int multiSampleQuality;
        PresentParameters presentParams;
        //Image dxErr;
        //Texture buffer;
        public string InitializeGraphics()
        {
            bool b = Manager.CheckDeviceType(0, DeviceType.Hardware, Format.X8R8G8B8, Format.X8R8G8B8, true);
            if (!b) return "No 3D Hardware"; //Hardware 3D not supported!
            try     //Should suit vast majority of (real) video devices...
            {
                // Test for antialias capability
                antiAlias = IsDeviceMultiSampleOK(DepthFormat.D16, Format.X8R8G8B8, multiSampleType);
                // Now  setup our D3D stuff
                presentParams = new PresentParameters();
                presentParams.Windowed = true;
                presentParams.SwapEffect = SwapEffect.Discard;
                presentParams.AutoDepthStencilFormat = DepthFormat.D16;
                presentParams.EnableAutoDepthStencil = true;
                if (antiAlias) presentParams.MultiSample = multiSampleType;
                m_device = new Device(0, DeviceType.Hardware, this,
                        CreateFlags.SoftwareVertexProcessing, presentParams);
                m_device.DeviceReset += new EventHandler(m_device_DeviceReset);
                return null;
            }
            catch (DirectXException ex)
            {
                return "DirectX Error:\n\"" + ex.ErrorString + "\"";
            }
        }

        //Antialias Test
        public bool IsDeviceMultiSampleOK(DepthFormat depthFmt, Format backbufferFmt, MultiSampleType multisampleType)
        {
            AdapterInformation ai = Manager.Adapters.Default;
            int result;
            multiSampleQuality = 0;
            // Verify that the render target surface supports the given multisample type
            if (Manager.CheckDeviceMultiSampleType(ai.Adapter, DeviceType.Hardware, backbufferFmt, true, multisampleType, out result, out multiSampleQuality))
            {
                // Verify that the depth stencil surface supports the given multisample type
                if (Manager.CheckDeviceMultiSampleType(ai.Adapter, DeviceType.Hardware, (Format)depthFmt, true, multisampleType, out result, out multiSampleQuality))
                    return true;    // if both calls succeed
            }
            return false;   // if either call fails.
        }

        Surface screenTarget;
        Surface printTarget;

        //DirectX DeviceReset callback
        void m_device_DeviceReset(object sender, EventArgs e)
        {
            m_device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, (float)this.Width/(float)this.Height, 1f, 4000f);
            CameraView = Matrix.LookAtLH(new Vector3(00, _viewHeight, _viewDistance), new Vector3(0, 128, 0), new Vector3(0, 1, 0));
            m_device.RenderState.Lighting = true;
            if (antiAlias) m_device.RenderState.MultiSampleAntiAlias = true;
            m_device.Lights[0].Type = LightType.Directional;
            m_device.Lights[0].Diffuse = Color.White;
            m_device.Lights[0].Direction = new Vector3(.8f, -1, -.8f);
            m_device.Lights[0].Enabled = true; 
            // Turn on the z-buffer.
            m_device.RenderState.ZBufferEnable = true;
            // Turn on ambient lighting.
            m_device.RenderState.Ambient = System.Drawing.Color.DimGray;
            //Texture filtering
            m_device.SamplerState[0].MinFilter = TextureFilter.Anisotropic;
            m_device.SamplerState[0].MagFilter = TextureFilter.Anisotropic;
            SetupPixelFog(BackColor, FogMode.Linear);
            //Create and set render target
            screenTarget = m_device.GetRenderTarget(0);
            printTarget = m_device.CreateRenderTarget(this.Width, this.Height, Format.X8R8G8B8, MultiSampleType.None, 0, true);
        }
        
        //-----------------------------------------------------------------
        //Object loaders and constructors

        //Bitmap from resources to texture image memory stream
        private MemoryStream getImageStream(Bitmap image)
        {
            MemoryStream memoryStream = new MemoryStream();
            image.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Bmp);
            memoryStream.Position = 0;
            return memoryStream;
        }

        //DX Mesh from resources as byte[] array - tries to find textures by name
        private void LoadMesh(byte[] xFile, ref Mesh mesh, ref Material[] meshmaterials, ref Texture[] meshtextures)
        {
            ExtendedMaterial[] materialarray;
            System.Resources.ResourceManager rm = Properties.Resources.ResourceManager;
            using (MemoryStream ms = new MemoryStream(xFile))
                mesh = Mesh.FromStream(ms, MeshFlags.Managed, m_device, out materialarray);
            if ((materialarray != null) && (materialarray.Length > 0))
            {
                //Load textures into materials
                meshmaterials = new Material[materialarray.Length];
                meshtextures = new Texture[materialarray.Length];
                for (int i = 0; i < materialarray.Length; i++)
                {
                    meshmaterials[i] = materialarray[i].Material3D;
                    meshmaterials[i].Ambient = meshmaterials[i].Diffuse;
                    string[] names = assembly.GetManifestResourceNames();
                    if ((materialarray[i].TextureFilename != null) && (materialarray[i].TextureFilename != string.Empty))
                    {
                        MemoryStream ms = null;
                        string name = materialarray[i].TextureFilename;
                        name = name.Substring(0, name.LastIndexOf('.')).Replace("-", "_");
                        try { ms = getImageStream((Bitmap)rm.GetObject(name)); }
                        catch
                        {
                            MessageBox.Show("Texture resource not found!\nFile : " + materialarray[i].TextureFilename + "\nNot present amongst resource definitions.", "Resource Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                            ms = getImageStream((Bitmap)rm.GetObject("system"));
                        }
                        if (ms != null) meshtextures[i] = TextureLoader.FromStream(m_device, ms);
                    }
                }
            }
            //Compute normals
            mesh = mesh.Clone(mesh.Options.Value, CustomVertex.PositionNormalTextured.Format, m_device);
            mesh.ComputeNormals();
            //Allow DX to optimize mesh
            int[] adjac = new int[mesh.NumberFaces * 3];
            mesh.GenerateAdjacency(0.5f, adjac);
            mesh.OptimizeInPlace(MeshFlags.OptimizeVertexCache, adjac);
        }

        //Basic draw mesh
        private void DrawMesh(Mesh mesh, Material[] meshmaterials, Texture[] meshtextures)
        {
            for (int i = 0; i < meshmaterials.Length; i++)
            {
                m_device.Material = meshmaterials[i];
                m_device.SetTexture(0, meshtextures[i]);
                mesh.DrawSubset(i);
            }
        }

        //Floor
        private Texture floorTexture;
        private Material floorMaterial;
        private CustomVertex.PositionNormalTextured[] floor;
        Size floorSize = new Size(5000, 5000); //Total size of floor
        float floorSquare = 0;          //Size of single floor square in pixels
        private void ConstructFloor()
        {
            floorMaterial = new Material();
            floorMaterial.Diffuse = Color.White;
            floorMaterial.Specular = Color.LightGray;
            floorMaterial.SpecularSharpness = 15.0F;
            MemoryStream ms = getImageStream(Properties.Resources.Alpha_Grid_8);
            floorTexture = TextureLoader.FromStream(m_device, ms);
            floorSquare = ((float)floorSize.Width / 30F);
            floor = new CustomVertex.PositionNormalTextured[6];
            //V0
            floor[0].Position = new Vector3((float)floorSize.Width, 0f, -(float)floorSize.Height);
            floor[0].Normal = new Vector3(0, 1, 0);
            floor[0].Tu = 0;
            floor[0].Tv = 10;
            //V1
            floor[1].Position = new Vector3(-(float)floorSize.Width, 0f, -(float)floorSize.Height);
            floor[1].Normal = new Vector3(0, 1, 0);
            floor[1].Tu = 10;
            floor[1].Tv = 10;
            //V2
            floor[2].Position = new Vector3((float)floorSize.Width, 0f, (float)floorSize.Height);
            floor[2].Normal = new Vector3(0, 1, 0);
            floor[2].Tu = 0;
            floor[2].Tv = 0;
            //V3
            floor[3].Position = new Vector3((float)floorSize.Width, 0f, (float)floorSize.Height);
            floor[3].Normal = new Vector3(0, 1, 0);
            floor[3].Tu = 10;
            floor[3].Tv = 10;
            //V4
            floor[4].Position = new Vector3(-(float)floorSize.Width, 0f, -(float)floorSize.Height);
            floor[4].Normal = new Vector3(0, 1, 0);
            floor[4].Tu = 0;
            floor[4].Tv = 0;
            //V5
            floor[5].Position = new Vector3(-(float)floorSize.Width, 0f, (float)floorSize.Height);
            floor[5].Normal = new Vector3(0, 1, 0);
            floor[5].Tu = 10;
            floor[5].Tv = 0;
        }

        //-- Aspect ratio lock --
        [StructLayout(LayoutKind.Sequential)]
        public struct WindowPos
        {
            public IntPtr hwnd;
            public IntPtr hwndInsertAfter;
            public int x;
            public int y;
            public int cx;
            public int cy;
            public uint flags;
        };
        protected override void WndProc(ref Message m)
        {
            if (_lockAspect && (m.Msg == 0x046 || m.Msg == 0x047)) //WM_WINDOWPOSCHANGING, WM_WINDOWPOSCHANGED
            {
                WindowPos mwp;
                mwp = (WindowPos)Marshal.PtrToStructure(m.LParam, typeof(WindowPos));
                mwp.cx = (int)(mwp.cy * (4f / 3f));
                Marshal.StructureToPtr(mwp, m.LParam, true);
                m.Result = (IntPtr)0;
            }
            if (m.Msg == 0x318) //WM_PRINTCLIENT
            {
                printClient(m.WParam, m.LParam);
            }
            base.WndProc(ref m);
        }

        private void printClient(IntPtr HDC, IntPtr options)
        {
            Graphics g = Graphics.FromHdcInternal(HDC);
            //set render target
            if (printTarget != null) m_device.SetRenderTarget(0, printTarget);
            Render();
            g.DrawImage(new Bitmap(SurfaceLoader.SaveToStream(ImageFileFormat.Bmp, printTarget)), 0, 0, this.Width, this.Height);
            m_device.SetRenderTarget(0, screenTarget);
            g.Dispose();
        }

        public override Size GetPreferredSize(Size proposedSize)
        {
            return new Size((int)(proposedSize.Height * (4f / 3f)), proposedSize.Height);
        }

        //Restrict angle (radians) to a 2 * PI range
        float twoPiRange(float angle)
        {
            while (angle < 0) angle += (float)(Math.PI * 2F);
            while (angle >= (float)(Math.PI * 2F)) angle -= (float)(Math.PI * 2F);
            return angle;
        }

        //Fog control
        void SetupPixelFog(Color color, FogMode Mode)
        {
            float Start = 3000f;    // For linear mode
            float End = 3500f;
            float Density = 0.66f;   // For exponential modes

            // Enable fog blending.
            m_device.SetRenderState(RenderStates.FogEnable, true);

            // Set the fog color.
            m_device.SetRenderState(RenderStates.FogColor, color.ToArgb());

            // Set fog parameters.
            if (Mode == FogMode.Linear)
            {
                m_device.SetRenderState(RenderStates.FogTableMode, (int)Mode);
                m_device.SetRenderState(RenderStates.FogStart, Start);
                m_device.SetRenderState(RenderStates.FogEnd, End);
            }
            else
            {
                m_device.SetRenderState(RenderStates.FogTableMode, (int)Mode);
                m_device.SetRenderState(RenderStates.FogDensity, Density);
            }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            if (dxBroken)
            {
                base.OnPaint(e);
                return;
            }
            Render();            
        }

        private void Render()
        {
            int result;
            m_device.CheckCooperativeLevel(out result); //Check directx device is working
            if ((ResultCode)result == ResultCode.Success)
            {
                try
                {
                    m_device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, BackColor, 1.0f, 0);
                    m_device.BeginScene();

                    //--- Vehicle positioning within scene
                    Matrix carPos, arrowPos;
                    //Precalculate attitude adjustments
                    float pitch = twoPiRange(_carPitch);    //Restrict pitch and roll
                    float roll = twoPiRange(_carRoll);
                    float pitchAbs = Math.Abs((float)Math.Sin(pitch));  //Absolute values of directional component displacements
                    float rollAbs = Math.Abs((float)Math.Sin(roll));
                    //Look at center of vehicle
                    float lookHeight = 128 + (pitchAbs * rollAbs * 224);
                    //Arrow half height when horizontal, full height when vertical
                    float arrowSize = 0.5F + (pitchAbs * rollAbs * 0.5F);
                    //Pitch adjustment
                    if (pitch < 0 || pitch > (float)(Math.PI))
                    {
                        //Rotate body pitch around rear wheels
                        carPos = Matrix.Translation(0, -128, 256) * Matrix.RotationX(pitch) * Matrix.Translation(0, 128, -256 * (float)Math.Cos(pitch));
                        //Arrow at rotational axis
                        if (roll >= (float)(Math.PI / 2F) && roll < (float)(Math.PI + (Math.PI / 2F))) arrowPos = Matrix.Translation(0, 128, 192);
                        else arrowPos = Matrix.Translation(0, 128, -256);
                    }
                    else
                    {
                        //Rotate body pitch around front wheels
                        carPos = Matrix.Translation(0, -128, -192) * Matrix.RotationX(pitch) * Matrix.Translation(0, 128, 192 * (float)Math.Cos(pitch));
                        //Arrow at rotational axis
                        if (roll >= (float)(Math.PI / 2F) && roll < (float)(Math.PI + (Math.PI / 2F))) arrowPos = Matrix.Translation(0, 128, -256);
                        else arrowPos = Matrix.Translation(0, 128, 192);
                    }
                    //Roll adjustment - rotate body yaw around lowest point of lowest wheel
                    // and raise body so lowest point just touches the floor
                    if (roll >= 0 && roll < (float)(Math.PI / 2F)) //Left front wheel
                        carPos *= Matrix.Translation(256, 0, 0) * Matrix.RotationZ(roll) * Matrix.Translation(-256 * (float)Math.Cos(roll) + (128 + (224 * pitchAbs)) * (float)Math.Sin(roll), 0, 0);
                    if (roll >= (float)(Math.PI / 2F) && roll < (float)(Math.PI)) //Left rear wheel
                        carPos *= Matrix.Translation(256, -256 - (448 * pitchAbs), 0) * Matrix.RotationZ(roll) * Matrix.Translation(-256 * (float)Math.Cos(roll) - (128 + (224 * pitchAbs)) * (float)Math.Sin(roll), 0, 0);
                    if (roll >= (float)(Math.PI) && roll < (float)(Math.PI + (Math.PI / 2F))) //Right rear wheel
                        carPos *= Matrix.Translation(-256, -256 - (448 * pitchAbs), 0) * Matrix.RotationZ(roll) * Matrix.Translation(256 * (float)Math.Cos(roll) - (128 + (224 * pitchAbs)) * (float)Math.Sin(roll), 0, 0);
                    if (roll >= (float)(Math.PI + (Math.PI / 2F)) && roll < (float)(Math.PI * 2F)) //Right front wheel
                        carPos *= Matrix.Translation(-256, 0, 0) * Matrix.RotationZ(roll) * Matrix.Translation(256 * (float)Math.Cos(roll) + (128 + (224 * pitchAbs)) * (float)Math.Sin(roll), 0, 0);
                    //Include user's positioning
                    carPos *= _carLocation;
                    //Position arrow inside vehicle then "unrotate" to point up
                    arrowPos = Matrix.Scaling(1, arrowSize, 1) * Matrix.RotationZ(-roll) * Matrix.RotationX(-pitch) * arrowPos * carPos;
                    //Camera based on lookHeight
                    CameraView = Matrix.LookAtLH(new Vector3(00, _viewHeight, _viewDistance), new Vector3(0, lookHeight, 0), new Vector3(0, 1, 0));

                    //--- Movement and steering
                    //Total distance is average of left and right
                    float avgDist = (_leftWheelDistance + _rightWheelDistance) / 2.0f;
                    //Rate of turning depends on wheelbase and floor friction - 4.0 seems ok
                    float turnAngle = twoPiRange((_leftWheelDistance - _rightWheelDistance) / 4.0f);
                    //distance travelled since last screen refresh
                    float diffDist = avgDist - lastWheelDistance;
                    //floor offset accumulators wrap into a single floor texture square
                    xOffs = ((float)Math.Sin(turnAngle) * diffDist * floorSquare + xOffs) % floorSquare;
                    yOffs = ((float)Math.Cos(turnAngle) * diffDist * floorSquare + yOffs) % floorSquare;
                    //Distance travelled to wheel rotation - depends on wheel size
                    float leftWheelAngle = twoPiRange(_leftWheelDistance);
                    float rightWheelAngle = twoPiRange(_rightWheelDistance);

                    //--- Start drawing
                    //Draw floor
                    m_device.Transform.World = Matrix.Translation(xOffs, 0, yOffs) * Matrix.RotationY(-turnAngle) * _worldLocation;
                    m_device.RenderState.Ambient = System.Drawing.Color.White;
                    m_device.VertexFormat = CustomVertex.PositionNormalTextured.Format;
                    m_device.Material = floorMaterial;
                    m_device.SetTexture(0, floorTexture);
                    m_device.DrawUserPrimitives(PrimitiveType.TriangleList, 2, floor);

                    //Draw vehicle
                    m_device.RenderState.Ambient = System.Drawing.Color.DimGray;
                    //Draw wheels
                    //Right front wheel
                    m_device.Transform.World = Matrix.Translation(-128F, -128F, 0) * Matrix.RotationX(-rightWheelAngle) * Matrix.Translation(-96F, 128F, -256F) * carPos * _worldLocation;
                    DrawMesh(wheelMesh, wheelMaterials, wheelTextures);
                    //Right rear wheel
                    m_device.Transform.World = Matrix.Translation(-128F, -128F, 0) * Matrix.RotationX(-rightWheelAngle) * Matrix.Translation(-96F, 128F, 192F) * carPos * _worldLocation;
                    DrawMesh(wheelMesh, wheelMaterials, wheelTextures);
                    //Left front wheel
                    m_device.Transform.World = Matrix.Translation(-128F, -128F, 0) * Matrix.RotationX(leftWheelAngle) * Matrix.RotationY((float)Math.PI) * Matrix.Translation(96F, 128F, -256F) * carPos * _worldLocation;
                    DrawMesh(wheelMesh, wheelMaterials, wheelTextures);
                    //Left rear wheel
                    m_device.Transform.World = Matrix.Translation(-128F, -128F, 0) * Matrix.RotationX(leftWheelAngle) * Matrix.RotationY((float)Math.PI) * Matrix.Translation(96F, 128F, 192F) * carPos * _worldLocation;
                    DrawMesh(wheelMesh, wheelMaterials, wheelTextures);

                    //Draw body
                    m_device.Transform.World = carPos * _worldLocation;
                    DrawMesh(carMesh, carMaterials, carTextures);

                    //Draw arrow
                    if (_enableArrow)
                    {
                        m_device.Transform.World = Matrix.RotationX(_arrowAngle) * arrowPos * _worldLocation;
                        DrawMesh(arrowMesh, arrowMaterials, arrowTextures);
                    }
                    m_device.EndScene();
                    //Try to render scene
                    m_device.Present();
                    lastWheelDistance = avgDist;
                }
                catch { } //Ignore LOST_DEVICEs that happen during this routine
                //finally
                //{
                //Draw to screen
                //e.Graphics.DrawImageUnscaled(new Bitmap(SurfaceLoader.SaveToStream(ImageFileFormat.Bmp, target)), new Point(0, 0));
                //}
            }
            //Eventually recover lost device
            else if ((ResultCode)result == ResultCode.DeviceNotReset) m_device.Reset(presentParams);
        }

        //Hack to propogate upward aspect ratio lock
        private void DxControl_SizeChanged(object sender, EventArgs e)
        {
            if (Owner != null)
                ((frmMain)Owner).resizeViewport(this.Width);
        }

        //AutoRefresh timer
        private void timer1_Tick(object sender, EventArgs e)
        {
            this.Invalidate();
        }

        bool clicked = false;
        bool dragging = false;
        Point clickPos;

        float oldViewAngle;
        float oldViewHeight;

        private void DxControl_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                clicked = true;
                clickPos = new Point(e.X, e.Y);
                oldViewAngle = ViewAngle;
                oldViewHeight = ViewHeight;
            }
        }

        private void DxControl_MouseMove(object sender, MouseEventArgs e)
        {
            if (clicked && !dragging)
                if (Math.Abs(clickPos.X - e.X) > 1 || Math.Abs(clickPos.Y - e.Y) > 1)
                    dragging = true;
            if (dragging) calculateCamera(e.X, e.Y);
        }

        private void calculateCamera(int x, int y)
        {
            float angle, height;
            angle = oldViewAngle + (float)((clickPos.X - x) / 2.0f);
            while (angle > 360) angle -= 360;
            while (angle < 0) angle += 360;
            ViewAngle = angle;
            height = oldViewHeight + (float)((clickPos.Y - y) * -5.0f);
            if (height > 1500) height = 1500;
            if (height < 50) height = 50;
            ViewHeight = height;
            if (!_autoRefresh) this.Invalidate();
        }

        private void DxControl_MouseUp(object sender, MouseEventArgs e)
        {
            dragging = false;
            clicked = false;
        }

        void DxControl_MouseWheel(object sender, MouseEventArgs e)
        {
            ViewDistance -= e.Delta;
            if (ViewDistance < 100) ViewDistance = 100;
            if (ViewDistance > 2500) ViewDistance = 2500;
            if (!_autoRefresh) this.Invalidate();
        }

        private void resetViewToolStripMenuItem_Click(object sender, EventArgs e)
        {
            ViewDistance = 1500;
            ViewHeight = 384;
            ViewAngle = 30;
            if (!_autoRefresh) this.Invalidate();
        }

    }
}
